﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.Text;
using System.Threading;
using LuaLanguageExtension.Parsing;
using PSLib.Parsing.Lua;
using Antlr.Runtime;
using PSLib.Parsing.Common.Search;
using PSLib.Parsing.Common;
using System.Collections;
using Microsoft.VisualStudio.Text.Tagging;
using LuaLanguageExtension.Classification;

namespace LuaLanguageExtension.Parsing
{
    internal class ObjectModelProvider : IDisposable
    {
        #region Constructor

        public ObjectModelProvider(ITextBuffer buffer)
        {
            this.buffer = buffer;
            this.activeStream = new CommonTokenStream();
            this.classificationCache = new Dictionary<Span, List<SpanClassificationTag>>();
            this.classificationCacheRefresh = new List<Span>();
            this.IntellisenseAcrossData = new ObjectModelIntellisenseAcrossData();

            buffer.Changed += new EventHandler<TextContentChangedEventArgs>(buffer_Changed);
            // we need to run rebuildNodes on a separate thread. Using timer
            // for this seems to be an overkill, but we need the timer anyway so - why not
            parserTimer = new Timer(RebuildTokenStreamAsynch, buffer.CurrentSnapshot, 0, Timeout.Infinite);

            dirty = false;
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            parserTimer.Dispose();
        }

        #endregion

        #region Event handlers

        private void buffer_Changed(object sender, TextContentChangedEventArgs e)
        {
            // shut down the old one 
            parserTimer.Dispose();

            // put the call to the rebuildNodes on timer
            parserTimer = new Timer(RebuildTokenStreamAsynch, e.After, PARSING_DELAY, Timeout.Infinite);
            lock (locker) dirty = true;
        }

        #endregion

        #region Private methods

        private void RebuildTokenStreamAsynch(object snapshotObject)
        {
            lock (locker) dirty = true;
            ITextSnapshot snapshot = (ITextSnapshot)snapshotObject;

            //Create stream from file
            Antlr.Runtime.ICharStream st = new Antlr.Runtime.ANTLRStringStream(snapshot.GetText());

            //Create lua lexer
            LuaLexer lexer = new LuaLexer(st);
            this.activeStream.TokenSource = lexer;
            this.activeStream.GetTokens();

            //Raise TokenStreamChanged event
            lock (locker) dirty = false;

            IList<SnapshotSpan> snapshotSpans2Refresh = null;
            lock ( this.classificationCacheRefresh)
            {
                snapshotSpans2Refresh = this.classificationCacheRefresh.Select(sp => new SnapshotSpan(snapshot, sp)).ToList();
                this.classificationCacheRefresh.Clear();
            }

            if (snapshotSpans2Refresh != null)
            {
                foreach (var item in snapshotSpans2Refresh)
                {
                    GetClassificationTagsFromActiveStream(item);
                }
            }

            RaiseTokenStreamChanged(snapshot);
        }

        internal void RaiseTokenStreamChanged(ITextSnapshot snapshot)
        {
            if (TokenStreamChanged != null) TokenStreamChanged(new SnapshotSpan(snapshot, 0, snapshot.Length));
        }

        private IEnumerable<ITagSpan<ClassificationTag>> GetClassificationTagsFromActiveStream(SnapshotSpan span)
        {
            TokensStreamSearchProvider search = activeStream.GetSearchProvider();
            RecognizedTokenRange tokensRange = null;
            try
            {
                tokensRange = search.Find(span.GetElementSpan());
            }
            catch (Exception)
            {
                //Abnormal situation     
            }
            if (tokensRange == null) yield return null;
            
            List<SpanClassificationTag> cacheSpan = null;
            if (this.classificationCache.ContainsKey(span.Span)) cacheSpan = this.classificationCache[span.Span];
            else
            {
                cacheSpan = new List<SpanClassificationTag>();
                this.classificationCache.Add(span.Span, cacheSpan);
            }

            int i = 0;
            foreach (RecognizedToken item in tokensRange.GetEnumeration(true))
            {
                if (item.NativeToken.Type != LuaLexer.WS)
                {
                    if ( i >= cacheSpan.Count) cacheSpan.Add(null);

                    if (cacheSpan[i] != null && cacheSpan[i].Hash == item.NativeToken.Text.GetHashCode())
                    {
                        yield return cacheSpan[i];
                    }
                    else
                    {
                        SnapshotSpan tokenSnapshotSpan = item.GetSnapshotSpan(span.Snapshot);
                        SpanClassificationTag tagSpan = new SpanClassificationTag(tokenSnapshotSpan, new ClassificationTag(Constants.LuaTokenClassificationTypes[item.Type]));
                        tagSpan.Hash = item.NativeToken.Text.GetHashCode();
                        cacheSpan[i] = tagSpan;
                        yield return tagSpan;
                    }
                    i++;
                }
            }
        }

        private IEnumerable<ITagSpan<ClassificationTag>> GetClassificationTagsFromLife(SnapshotSpan span)
        {
            ITextSnapshotLine line = span.Start.GetContainingLine();
            string lineText = line.GetText();

            //Source code has not been parsed yet. So using fast coloring.
            //Create stream from new text
            Antlr.Runtime.ICharStream st = new Antlr.Runtime.ANTLRStringStream(lineText);
            //Create lua lexer
            LuaLexer lexer = new LuaLexer(st);
            CommonTokenStream localStream = new CommonTokenStream();
            localStream.TokenSource = lexer;

            TokensStreamSearchProvider search = localStream.GetSearchProvider();
            foreach (IToken item in localStream.GetTokens())
            {
                if (item.Type != LuaLexer.WS)
                {
                    RecognizedToken recTok = TokensRecognizer.Recognize(item);
                    Span tokenSpan = new Span(line.Start.Position + recTok.NativeToken.CharPositionInLine, recTok.NativeToken.Text.Length);
                    if (tokenSpan.IntersectsWith(span))
                    {
                        SnapshotSpan tokenSnapshotSpan = new SnapshotSpan(span.Snapshot, tokenSpan);
                        TagSpan<ClassificationTag> tagSpan = new TagSpan<ClassificationTag>(tokenSnapshotSpan, new ClassificationTag(Constants.LuaTokenClassificationTypes[recTok.Type]));
                        yield return tagSpan;
                    }
                }
            }

            lock (this.classificationCacheRefresh)
            {
                if(!this.classificationCacheRefresh.Contains(span.Span))
                    this.classificationCacheRefresh.Add(span.Span);
            }
        }

        #endregion

        #region Public Methods

        public IEnumerable<ITagSpan<ClassificationTag>> GetClassificationTags(SnapshotSpan span)
        {
            IEnumerable<ITagSpan<ClassificationTag>> resultTags = null;
            if (!dirty) resultTags = GetClassificationTagsFromActiveStream(span);
            else resultTags = GetClassificationTagsFromLife(span);
            foreach (var item in resultTags)
	        {
                yield return item;
	        }
        }

        #endregion

        #region Properties

        public ObjectModelIntellisenseAcrossData IntellisenseAcrossData { get; private set; }

        #endregion

        #region Events

        public delegate void SnapshotEvent(SnapshotSpan snapshotSpan);
        public event SnapshotEvent TokenStreamChanged;

        #endregion

        private object locker = new object();
        private bool dirty;
        private ITextBuffer buffer;
        private CommonTokenStream activeStream;
        private Dictionary<Span, List<SpanClassificationTag>> classificationCache;
        private List<Span> classificationCacheRefresh;
        
        /// <summary>
        /// indicates the delay (in milliseconds) of parser invoking. 
        /// </summary>
        private const int PARSING_DELAY = 1000;
        /// <summary>
        /// The timer for optimization the parsing process. If there would be some changes with time 
        /// between sequential changes less then PARSING_DELAY, then rebuild process would be invoked only once.
        /// </summary>
        private Timer parserTimer;
    }
}
